﻿using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;

namespace Orz.Framework.Controls
{
    /// <summary>
    /// Represents a control designed for entering and handling passwords.
    /// </summary>
    public class PasswordBox : TextBox
    {
        #region Properties

        /// <summary>
        /// Gets or sets the display length.
        /// </summary>
        /// <value>The display length.</value>
        public int DisplayLength { get; set; }

        /// <summary>
        /// Gets or sets the masking char.
        /// </summary>
        /// <value>A masking character to echo when the user enters text into the 
        /// Orz.Framework.Controls.PasswordBox.The default value is a bullet character (●).</value>
        public char PasswordChar { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether this instance is in masking.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this instance is n masking; otherwise, <c>false</c>.
        /// </value>
        public bool IsMasking { get; private set; }

        /// <summary>
        /// Gets or sets the text contents of the text box. This is a dependency property.
        /// </summary>
        /// <value></value>
        /// <returns>A string containing the text contents of the text box. The default is an empty string ("").</returns>
        [Bindable(false), Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
        [Obsolete("This property is obsoleted, please use Password instead.", true)]
        public new string Text { get; set; }

        /// <summary>
        /// Gets or sets the behavior.
        /// </summary>
        /// <value>The behavior.</value>
        public DisplayBehavior Behavior
        {
            get { return (DisplayBehavior)GetValue(BehaviorProperty); }
            set { SetValue(BehaviorProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Behavior.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty BehaviorProperty =
            DependencyProperty.Register("Behavior", typeof(DisplayBehavior), typeof(PasswordBox), new UIPropertyMetadata(DisplayBehavior.Default, new PropertyChangedCallback((obj, e) =>
            {
                PasswordBox pb = (PasswordBox)obj;
                if (pb.autoMaskTimer.IsEnabled)
                {
                    pb.autoMaskTimer.Stop();
                    pb.MaskText();
                    pb.autoMaskTimer.Start();
                }
            })));

        /// <summary>
        /// Gets or sets the password.
        /// </summary>
        /// <value>The password.</value>
        public string Password
        {
            get { return (string)GetValue(PasswordProperty); }
            set
            {
                if (value == null)
                    return;
                SetValue(PasswordProperty, value);
                base.Text = value;
                this.SelectionStart = base.Text.Length;
            }
        }

        // Using a DependencyProperty as the backing store for Password.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty PasswordProperty =
            DependencyProperty.Register("Password", typeof(string), typeof(PasswordBox), new UIPropertyMetadata(string.Empty));

        /// <summary>
        /// Gets or sets the auto mask interval.
        /// </summary>
        /// <value>The auto mask interval.</value>
        public TimeSpan AutoMaskInterval
        {
            get { return (TimeSpan)GetValue(AutoMaskIntervalProperty); }
            set { SetValue(AutoMaskIntervalProperty, value); }
        }

        // Using a DependencyProperty as the backing store for AutoMaskInterval.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty AutoMaskIntervalProperty =
            DependencyProperty.Register("AutoMaskInterval", typeof(TimeSpan), typeof(PasswordBox), new UIPropertyMetadata(TimeSpan.FromMilliseconds(1000), new PropertyChangedCallback((obj, e) =>
            {
                PasswordBox pb = (PasswordBox)obj;
                pb.autoMaskTimer.Interval = (TimeSpan)pb.GetValue(AutoMaskIntervalProperty);
            })));

        #endregion

        #region Fields

        /// <summary>
        /// The timer control the text masking automatic.
        /// </summary>
        DispatcherTimer autoMaskTimer;
        /// <summary>
        /// If password is set before loaded, this parameter will cache the value.
        /// </summary>
        string pwdPreLoad;

        #endregion

        #region Constructs

        static PasswordBox()
        {
            //DefaultStyleKeyProperty.OverrideMetadata(typeof(PasswordBox), new FrameworkPropertyMetadata(typeof(PasswordBox)));
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="PasswordBox"/> class.
        /// </summary>
        public PasswordBox()
        {
            PasswordChar = '●';
            DisplayLength = 1;
            autoMaskTimer = new DispatcherTimer() { Interval = AutoMaskInterval };

            autoMaskTimer.Tick += (sender, e) =>
            {
                autoMaskTimer.Stop();
                MaskFull();
            };
            this.Loaded += (sender, e) =>
            {
                if (!string.IsNullOrEmpty(Password))
                {
                    BeginMasking();
                    base.Text = Password;
                    this.SelectionStart = base.Text.Length;
                    EndMasking();
                    autoMaskTimer.Start();
                }
            };
        }
        #endregion

        #region Overrides

        /// <summary>
        /// Is called when content in this editing control changes.
        /// </summary>
        /// <param name="e">The arguments that are associated with the <see cref="E:System.Windows.Controls.Primitives.TextBoxBase.TextChanged"/> event.</param>
        protected override void OnTextChanged(TextChangedEventArgs e)
        {
            // If is not in masking, try to process.
            if (!IsMasking)
            {
                autoMaskTimer.Stop();
                TextChange[] changeList = new TextChange[e.Changes.Count];
                e.Changes.CopyTo(changeList, 0);
                TextChange change = changeList[0];

                SetPassword(change.Offset, -change.RemovedLength);
                SetPassword(change.Offset, change.AddedLength);
                MaskText();
                if (!this.IsLoaded)
                    pwdPreLoad = base.Text;
                e.Handled = true;
                autoMaskTimer.Start();
            }
        }

        /// <summary>
        /// Invoked when an unhandled <see cref="E:System.Windows.Input.Keyboard.PreviewKeyDown"/> attached event reaches an element in its route that is derived from this class. Implement this method to add class handling for this event.
        /// </summary>
        /// <param name="e">The <see cref="T:System.Windows.Input.KeyEventArgs"/> that contains the event data.</param>
        protected override void OnPreviewKeyDown(System.Windows.Input.KeyEventArgs e)
        {
            if (e.KeyboardDevice.Modifiers == System.Windows.Input.ModifierKeys.Control)
            {
                // disable Ctrl + V/X/C
                if (e.Key == System.Windows.Input.Key.V
                    || e.Key == System.Windows.Input.Key.X
                    || e.Key == System.Windows.Input.Key.C)
                {
                    e.Handled = true;
                }
            }
            base.OnPreviewKeyDown(e);
        }

        /// <summary>
        /// Called whenever an unhandled <see cref="E:System.Windows.FrameworkElement.ContextMenuOpening"/> routed event reaches this class in its route. Implement this method to add class handling for this event.
        /// </summary>
        /// <param name="e">Arguments of the event.</param>
        protected override void OnContextMenuOpening(ContextMenuEventArgs e)
        {
            // disable the context menu(disable the copy/cut/paste).
            e.Handled = true;
            base.OnContextMenuOpening(e);
        }
        
        #endregion

        #region Private Methods

        /// <summary>
        /// Mask the full text.
        /// </summary>
        private void MaskFull()
        {
            BeginMasking();
            int selectedIndex = this.SelectionStart;
            base.Text = new string(PasswordChar, base.Text != null ? base.Text.Length : 0);
            this.SelectionStart = selectedIndex;
            EndMasking();
        }

        /// <summary>
        /// Masks the text.
        /// </summary>
        private void MaskText()
        {
            BeginMasking();
            int displayLength = DisplayLength;
            if (displayLength < 0)
                displayLength = 0;

            int prefixMaskEnd = this.SelectionStart - displayLength;
            int prefixOffset = 0;
            if (prefixMaskEnd < 0)
            {
                prefixOffset = prefixMaskEnd;
                prefixMaskEnd = 0;
            }

            int postMaskStart = this.SelectionStart + displayLength;
            int postOffset = 0;
            if (postMaskStart > base.Text.Length)
            {
                postOffset = base.Text.Length - postMaskStart;
                postMaskStart = base.Text.Length;
            }

            string prefixMaskText = new string(PasswordChar, prefixMaskEnd);
            string postMaskText = new string(PasswordChar, base.Text.Length - postMaskStart);

            int selectedIndex = this.SelectionStart;

            string before = Password.Substring(prefixMaskEnd, displayLength + prefixOffset);

            string after = new string(PasswordChar, this.SelectionStart == base.Text.Length ? 0 : displayLength);
            if ((Behavior & DisplayBehavior.Before) != DisplayBehavior.Before)
                after = Password.Substring(this.SelectionStart, displayLength + postOffset);

            base.Text = prefixMaskText
                + before
                + after
                + postMaskText;
            this.SelectionStart = selectedIndex;

            EndMasking();
        }

        /// <summary>
        /// Sets the password.
        /// </summary>
        /// <param name="startIndex">The start index.</param>
        /// <param name="changedLength">Length of the changed.</param>
        private void SetPassword(int startIndex, int changedLength)
        {
            if (changedLength > 0)
            {
                this.SetValue(PasswordProperty, Password.Insert(Math.Min(startIndex, Password.Length), base.Text.Substring(startIndex, changedLength)));
            }
            else if (changedLength < 0)
            {
                this.SetValue(PasswordProperty, Password.Remove(Math.Min(startIndex, Password.Length - 1), -changedLength));
            }
        }

        /// <summary>
        /// Ends the masking.
        /// </summary>
        private void EndMasking()
        {
            IsMasking = false;
        }

        /// <summary>
        /// Begins the masking.
        /// </summary>
        private void BeginMasking()
        {
            IsMasking = true;
        }
        #endregion
    }

    /// <summary>
    /// The behavior of password displays.
    /// </summary>
    public enum DisplayBehavior
    {
        /// <summary>
        /// Display the current input character and the next character(if existed).
        /// </summary>
        Default = 0x00,
        /// <summary>
        /// Only display the current input character.
        /// </summary>
        Before = 0x01
    }
}
